/* Copyright (c) 2006, 2009, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */
 
package com.cburch.logisim.tools;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.Wire;
import com.cburch.logisim.comp.Component;
import com.cburch.logisim.data.AbstractAttributeSet;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.AttributeEvent;
import com.cburch.logisim.data.AttributeListener;
import com.cburch.logisim.data.AttributeSet;
import com.cburch.logisim.gui.main.AttributeTable;
import com.cburch.logisim.gui.main.AttributeTableListener;
import com.cburch.logisim.gui.main.Canvas;
import com.cburch.logisim.gui.main.Selection;
import com.cburch.logisim.proj.Project;
import com.cburch.logisim.collections.UnmodifiableList;

class SelectionAttributes extends AbstractAttributeSet
        implements AttributeTableListener {
    private static final Attribute[] EMPTY_ATTRIBUTES = new Attribute[0];
    private static final Object[] EMPTY_VALUES = new Object[0];
    
    private class Listener implements Selection.Listener, AttributeListener {
        public void selectionChanged(Selection.Event e) {
            updateList(true);
        }

        public void attributeListChanged(AttributeEvent e) {
            if(listening) updateList(false);
        }

        public void attributeValueChanged(AttributeEvent e) {
            if(listening) updateList(false);
        }
    }
    
    private Listener listener;
    private boolean listening;
    private Project project;
    private Circuit circuit;
    private Selection oldSelection;
    private Set selected;
    private Attribute[] attrs;
    private boolean[] readOnly;
    private Object[] values;
    private List attrsView;
    
    public SelectionAttributes() {
        this.listener = new Listener();
        this.listening = true;
        this.oldSelection = null;
        this.selected = Collections.EMPTY_SET;
        this.attrs = EMPTY_ATTRIBUTES;
        this.values = EMPTY_VALUES;
        this.attrsView = Collections.EMPTY_LIST;
    }
    
    void setCanvas(Canvas value) {
        Selection oldSel = oldSelection;
        Selection newSel = value.getSelection();
        project = value.getProject();
        circuit = value.getCircuit();
        if(newSel != oldSel) {
            if(oldSel != null) oldSel.removeListener(listener);
            oldSelection = newSel;
            if(newSel != null) newSel.addListener(listener);
            updateList(true);
        }
    }
    
    void setListening(boolean value) {
        if(listening != value) {
            listening = value;
            if(value) updateList(false);
        }
    }
    
    private void updateList(boolean ignoreIfSelectionSame) {
        Selection selection = oldSelection;
        Set oldSel = selected;
        Set newSel = selection == null ? Collections.EMPTY_SET
                : createSet(selection.getComponents());
        if(haveSameElements(newSel, oldSel)) {
            if(ignoreIfSelectionSame) return;
            newSel = oldSel;
        } else {
            for(Iterator it = oldSel.iterator(); it.hasNext(); ) {
                Object o = it.next();
                if(!newSel.contains(o)) {
                    ((Component) o).getAttributeSet().removeAttributeListener(listener);
                }
            }
            for(Iterator it = newSel.iterator(); it.hasNext(); ) {
                Object o = it.next();
                if(!oldSel.contains(o)) {
                    ((Component) o).getAttributeSet().addAttributeListener(listener);
                }
            }
        }
        
        LinkedHashMap attrMap = computeAttributes(newSel);
        boolean same = isSame(attrMap, this.attrs, this.values);

        if(same) {
            if(newSel != oldSel) this.selected = newSel;
        } else {
            Attribute[] newAttrs = new Attribute[attrMap.size()];
            Object[] newValues = new Object[newAttrs.length];
            boolean[] newReadOnly = new boolean[newAttrs.length];
            int i = 0;
            for(Iterator it = attrMap.entrySet().iterator(); it.hasNext(); i++) {
                Map.Entry entry = (Map.Entry) it.next();
                newAttrs[i] = (Attribute) entry.getKey();
                newValues[i] = entry.getValue();
                newReadOnly[i] = computeReadOnly(newSel, newAttrs[i]);
            }
            if(newSel != oldSel) this.selected = newSel;
            this.attrs = newAttrs;
            this.attrsView = new UnmodifiableList(newAttrs);
            this.values = newValues;
            this.readOnly = newReadOnly;
            fireAttributeListChanged();
        }
    }
    
    private static Set createSet(Collection comps) {
        boolean includeWires = true;
        for(Iterator sit = comps.iterator(); sit.hasNext(); ) {
            if(!(sit.next() instanceof Wire)) { includeWires = false; break; }
        }
        
        if(includeWires) {
            return new HashSet(comps);
        } else {
            HashSet ret = new HashSet();
            for(Iterator sit = comps.iterator(); sit.hasNext(); ) {
                Object next = sit.next();
                if(!(next instanceof Wire)) ret.add(next);
            }
            return ret;
        }
    }

    private static boolean haveSameElements(Collection a, Collection b) {
        if(a == null) {
            return b == null ? true : b.size() == 0;
        } else if(b == null) {
            return a.size() == 0;
        } else if(a.size() != b.size()) {
            return false;
        } else {
            for(Iterator it = a.iterator(); it.hasNext(); ) {
                Object item = it.next();
                if(!b.contains(item)) return false;
            }
            return true;
        }
    }
    
    private static LinkedHashMap computeAttributes(Collection newSel)  {
        LinkedHashMap attrMap = new LinkedHashMap();
        Iterator sit = newSel.iterator();
        if(sit.hasNext()) {
            AttributeSet first = ((Component) sit.next()).getAttributeSet();
            for(Iterator ait = first.getAttributes().iterator(); ait.hasNext(); ) {
                Attribute attr = (Attribute) ait.next();
                attrMap.put(attr, first.getValue(attr));
            }
            while(sit.hasNext()) {
                AttributeSet next = ((Component) sit.next()).getAttributeSet();
                Iterator ait = attrMap.keySet().iterator();
                while(ait.hasNext()) {
                    Attribute attr = (Attribute) ait.next();
                    if(next.containsAttribute(attr)) {
                        Object v = attrMap.get(attr);
                        if(v != null && !v.equals(next.getValue(attr))) {
                            attrMap.put(attr, null);
                        }
                    } else {
                        ait.remove();
                    }
                }
            }
        }
        return attrMap;
    }
    
    private static boolean isSame(LinkedHashMap attrMap, Attribute[] oldAttrs,
            Object[] oldValues) {
        if(oldAttrs.length != attrMap.size()) {
            return false;
        } else {
            Iterator it = attrMap.entrySet().iterator();
            for(int j = 0; j < oldAttrs.length && it.hasNext(); j++) {
                Map.Entry entry = (Map.Entry) it.next();
                Attribute a = (Attribute) entry.getKey();
                if(oldAttrs[j] != a || j >= oldValues.length) return false;
                Object ov = oldValues[j];
                Object nv = entry.getValue();
                if(ov == null ? nv != null : !ov.equals(nv)) return false;
            }
            return true;
        }
    }
    
    private static boolean computeReadOnly(Collection sel, Attribute attr) {
        for(Iterator it = sel.iterator(); it.hasNext(); ) {
            AttributeSet attrs = ((Component) it.next()).getAttributeSet();
            if(attrs.isReadOnly(attr)) return true;
        }
        return false;
    }

    protected void copyInto(AbstractAttributeSet dest) {
        throw new UnsupportedOperationException("SelectionAttributes.copyInto");
    }

    public List getAttributes() {
        return attrsView;
    }
    
    public boolean isReadOnly(Attribute attr) {
        int i = findIndex(attr);
        boolean[] ro = readOnly;
        return i >= 0 && i < ro.length ? ro[i] : true;
    }

    public Object getValue(Attribute attr) {
        int i = findIndex(attr);
        Object[] vs = values;
        return i >= 0 && i < vs.length ? vs[i] : null;
    }

    public void setValue(Attribute attr, Object value) {
        int i = findIndex(attr);
        Object[] vs = values;
        if(i >= 0 && i < vs.length) {
            vs[i] = value;
            for(Iterator it = selected.iterator(); it.hasNext(); ) {
                Component comp = (Component) it.next();
                comp.getAttributeSet().setValue(attr, value);
            }
        }
    }
    
    private int findIndex(Attribute attr) {
        Attribute[] as = attrs;
        for(int i = 0; i < as.length; i++) {
            if(attr == as[i]) return i;
        }
        return -1;
    }

    public void valueChangeRequested(AttributeTable table, AttributeSet attrs,
            Attribute attr, Object value) {
        project.doAction(new SelectionAttributeChange(this, circuit, selected,
                attr, value));
    }
}
